#!/usr/bin/env perl
use 5.018;
use warnings;
use utf8;
use lib 'lib';
#run script under mojo/dtask dir (has lib dir)
#use DTask::Task;
use DTask::Link;

use Gerrit::API;

use Gerrit::Link;
use Mango;
use Mango::BSON qw(bson_true bson_false);
my $mango = Mango->new;
DTask::Link::setMango($mango);

use Mojo::IOLoop;
use Mojo::IOLoop::ReadWriteFork;

use BugzillaJsonRpc::Bug;
use BugzillaJsonRpc::User;
BugzillaJsonRpc::User::autoLogin;

#use Mojo::JSON qw(decode_json);
use JSON::XS qw(decode_json);
use List::Util qw(min);

#bug status
use constant {
	BUG_STATUS_ERROR => 0,
	BUG_IN_PROGRESS => 1,
	BUG_REOPENED => 2,
	BUG_FIXED => 3,
	BUG_VERIFIED => 4,
	BUG_MERGED => 5,
};
use constant BUG_STATUS => qw(BUG_STATUS_ERROR BUG_IN_PROGRESS BUG_REOPENED BUG_FIXED BUG_VERIFIED BUG_MERGED);

my $on_read_flag = 0;
my $json_coder = JSON::XS->new->ascii->pretty->allow_nonref;

use Data::Dumper;

#CONFIG
use Config::Tiny;
my $cfg_fpath = "$ENV{HOME}/.config/dtask/gerrit_event_watcher.cfg";
my $cfg = Config::Tiny->read($cfg_fpath)
	or die "can not read config file $cfg_fpath : $!";

my $username = $cfg->{_}{username};
my $ssh_port = $cfg->{_}{ssh_port};
my $ssh_host = $cfg->{_}{ssh_host};

my $fork = Mojo::IOLoop::ReadWriteFork->new;

sub changeBugsStatus
{
	my ($cl_num, $cb) = @_;

	warn 'changeBugsStatus CL '. $cl_num ;

	Mojo::IOLoop->delay(
		sub {
			my ($delay) = @_;
			my $end = $delay->begin(0);
			DTask::Link::getLinks('gerrit', $cl_num, 'bugzilla', sub {
				$end->(@_);
			});
		},
		sub {
			my ($delay, $bugs, $err) = @_;
			if ($err)
			{
				warn "step 1 error";
				return;
			}
			if (defined $bugs && @$bugs)
			{
				for my $bug_id (@$bugs)
				{
					my $end = $delay->begin(0);
					DTask::Link::getLinks('bugzilla', $bug_id, 'gerrit', sub {
						my ($gerrits, $err) = @_;

						Mojo::IOLoop->delay(
							sub {
								my ($_delay) = @_;
								my $_end = $_delay->begin(0);
								for (@$gerrits)
								{
									Gerrit::API::getChangeInfo($cl_num, o => 'DETAILED_LABELS', sub {
										my ($ret) = @_;
										unless ($ret->{error})
										{
											$ret->{result} = calcBugStatus($ret->{result});
										}
										$_end->($ret);
									});
								}
							},
							sub {
								my ($delay, @infos) = @_;
								warn "dump \@infos";
								warn Dumper \@infos;
								
								my @err_msgs = grep { $_->{error_message} if $_->{error} } @infos;
								if (@err_msgs)
								{
									warn "on error";
									$end->([$bug_id, [ BUG_STATUS_ERROR , join("\n", @err_msgs) ] ]);
								}
								else
								{
									my @bug_status = map { $_->{result} } grep { not $_->{error} } @infos;
									my $bug_final_status = getBugFinalStatus(@bug_status);
									warn "bug $bug_id finall status is ". (BUG_STATUS)[ $bug_final_status->[0] ];
									
									$end->([$bug_id, $bug_final_status]);
								}
							}
						);
					});	
				}
			}
			else
			{
				warn "gerrit $cl_num do not have any bugs link";
			}
			
		},

		sub {
			my ($delay, @infos) = @_;

			for (@infos)
			{
				my ($bug_id, $bug_final_status) = @$_;
				warn "apply status change to bugzilla bug $bug_id";
				changeBugzillaBugStatus($bug_id, $bug_final_status, $delay->begin );
			}
			
		}

	);

					
}

sub calcBugStatus
{
# BUG_IN_PROGRESS, BUG_REOPENED, BUG_FIXED,BUG_VERIFIED,BUG_MERGED
	my ($detail) = @_;
	my $status = $detail->{status};
	my $cl_num = $detail->{_number};
	my $cl_url = Gerrit::API::getChangeUrl($cl_num);
	my $status_code;
	my $status_str = "Gerrit CL $cl_num ($cl_url) ";

	if ($status eq 'NEW' )
	{
		my $verified_value = Gerrit::API::getChangeVerifiedValue($detail);
		if ($verified_value > 0 )
		{
			$status_str .= "verified OK";
			$status_code = BUG_VERIFIED;
		}
		elsif( $verified_value == 0 )
		{
			$status_str .= "verified Zero";
			$status_code = BUG_FIXED;
		}
		elsif( $verified_value < 0 )
		{
			$status_str .= "verified failed";
			$status_code = BUG_REOPENED;
		}

	}
	elsif( $status eq 'MERGED')
	{
		$status_str .= "merged";
		$status_code = BUG_MERGED;
	}
	elsif( $status eq 'ABANDONED')
	{
		$status_str .= "abandoned";
		$status_code = BUG_IN_PROGRESS;
	}

	warn "calcBugStatus : CL $cl_num -> bug status ". (BUG_STATUS)[$status_code];
	return [ $status_code, $status_str ];
}

sub getBugFinalStatus
{
	my (@bug_status) = @_;
	my $status_code = min( map { $_->[0] } @bug_status );
	my $status_str = join("\n", map { $_->[1] } @bug_status);
	return [$status_code, $status_str];
}

sub changeBugzillaBugStatus
{
	my ($bug_id, $status, $cb) = @_;
	my ($status_code, $status_str) = @$status;
	my $params;
	if ( $status_code == BUG_IN_PROGRESS )
	{
		$params = {
			status => 'IN_PROGRESS',
		};
	}
	elsif ( $status_code == BUG_REOPENED )
	{
		$params = {
			status => 'REOPENED',
		};
	}
	elsif ($status_code == BUG_FIXED )
	{
		$params = {
			status => 'RESOLVED',
			resolution => 'FIXED',
		};
	}
	elsif ( $status_code == BUG_VERIFIED )
	{
		$params = {
			status => 'VERIFIED',
			resolution => 'FIXED',
		};
	}
	elsif ( $status_code == BUG_MERGED )
	{
		$params = {
			status => 'MERGED',
		};
	}

	my $status_op_str = $params->{status} . " " . ($params->{resolution} // "");
	$params->{comment}{body} =  "$status_str\nChange Bug Status to $status_op_str";

	warn "changeBugzillaBugStatus: bug: $bug_id message: $status_str";
	BugzillaJsonRpc::Bug::update($bug_id, $params, sub {
		warn 'bug.Update done';
		$cb->()
	} );
}

sub processRevert
{
	my ($cl_num, $commit_id, $cb) = @_;

	my $process_revert_end = sub {
		DTask::Link::setAttr('gerrit', $cl_num, {'revert_processed' => bson_true }, sub {
			warn "processRevert cl $cl_num done";
			$cb->();
		});
	};

	DTask::Link::getAttr('gerrit', $cl_num, 'revert_processed', sub {
		my ($revert_processed, $err) = @_;
		if ($revert_processed)
		{
			warn "gerrit $cl_num revert have processed";
			$cb->();
		}
		else
		{
			Gerrit::API::getChangeByCommitId($commit_id, sub {
				my ($ret) = @_;
				unless ($ret->{error})
				{
					my $reverted_cl_num = $ret->{result}{_number};

					DTask::Link::getLinks('gerrit', $reverted_cl_num, 'bugzilla', sub {
						my ($bugs, $err) = @_;
						if (defined $bugs && @$bugs)
						{
							my $cl_url = Gerrit::API::getChangeUrl($cl_num);
							my $reverted_cl_url = Gerrit::API::getChangeUrl($reverted_cl_num);
							my $status_str = "Gerrit CL $reverted_cl_num ($reverted_cl_url) "
								."reverted by CL $cl_num ($cl_url)";

							changeBugzillaBugStatus($bugs, [ BUG_IN_PROGRESS, $status_str ], sub {
								warn "change bugs to in progresss";
								$process_revert_end->();
							});
						}
						else
						{
							$process_revert_end->();
						}

					});
				}
			});
		}
	});

}

sub processAbandoned
{
	my ($cl_num, $cb) = @_;
	DTask::Link::unlinkAll('gerrit', $cl_num, sub {
		warn "processAbandoned: unlinkAll CL $cl_num done";	
		$cb->();
	});
}

sub getRevertedCommitId
{
	my ($commit_message) = @_;
	if ( $commit_message =~ /This reverts commit ([0-9a-f]{40})/ )
	{
		return $1;
	}
}

sub processBuffer
{
	my ($buffer, $cb) = @_;
	my $end = sub {
		warn "processBuffer done";
		$cb->();
	};

	my $info = eval { decode_json($buffer) };
	if ( ! defined $info )
	{
		warn "buffer is not json";
		say STDERR "buffer is ". $buffer;
		$end->();
		return;
	}


	my $pretty_printed_json_str = $json_coder->encode($info);

	say $pretty_printed_json_str;

	my $type = $info->{type};
	my $approvals = $info->{approvals};

	my $comment = $info->{comment} // "";

	warn "TYPE: $type\nCOMMENT:$comment\n";

	my $change = $info->{change};
	my $cl_num = $change->{number};
	my $commit_message = $change->{commitMessage};

	if ( $type eq 'patchset-created')
	{
		if ( my $commit_id = getRevertedCommitId( $commit_message ) )
		{
			processRevert($cl_num, $commit_id, $end);
		}
		else
		{
			Gerrit::Link::createLinkByCLCommitMessage($cl_num, $commit_message, $end);
		}
	}
	elsif ( $type eq 'comment-added')
	{
		
		my $verified_flag = 0;

		for ( @$approvals )
		{
			if ( $_->{type} eq "Verified")
			{
				$verified_flag = 1;
			}
		}
		if ( $comment =~ m/Patch Set.*-Verified/ )
		{
			$verified_flag = 1;
		}

		if ($verified_flag)
		{
			changeBugsStatus($cl_num, $end);
		}
		else
		{
			$end->();
		}


	}
	elsif ( $type eq 'change-merged' )
	{
		changeBugsStatus($cl_num, $end);
	}
	elsif ( $type eq 'change-abandoned' )
	{
		if ( my $commit_id = getRevertedCommitId($commit_message))
		{
			DTask::Link::getAttr('gerrit', $cl_num, 'revert_processed', sub {
				my ($revert_processed, $err) = @_;
				if ($revert_processed)
				{
					DTask::Link::setAttr('gerrit', $cl_num,{revert_processed => bson_false}, sub {
						Gerrit::API::getChangeByCommitId($commit_id, sub {
							my ($ret) = @_;
							unless ($ret->{error})
							{
								my $reverted_cl_num = $ret->{result}{_number};
								warn "restore the bug status which link the reverted gerrit $reverted_cl_num to MERGED";
								changeBugsStatus($reverted_cl_num, $end );
							}
							else
							{
								$end->();
							}
								
						});
					});
				}
				else
				{
					$end->();
				}
			});

		}
		else
		{
			changeBugsStatus($cl_num, sub {
				processAbandoned($cl_num, $end);
			});
		}

	}
	elsif ($type eq 'change-restored')
	{
		if ( my $commit_id = getRevertedCommitId( $commit_message ) )
		{
			processRevert($cl_num, $commit_id, $end);
		}
		else
		{
			Gerrit::Link::createLinkByCLCommitMessage($cl_num, $commit_message, sub {
				changeBugsStatus($cl_num, $end);
			});
		}
	}
}

$fork->on(error => sub {
	my ($fork, $err) = @_;
	warn "fork error ". $err;
});

$fork->on(close => sub {
	my ($fork, $exit_value, $signal) = @_;
	warn "fork pipe closed (exit value $exit_value, signal $signal), quit";
	my $reactor = $fork->ioloop->reactor;
	$reactor->stop;
});


sub safeKill
{
	Mojo::IOLoop->recurring(0.5, sub {
		if ( $on_read_flag )
		{
			warn "safeKill: processing buffer, wait next";
		}
		else
		{
			warn "safeKill: kill fork process";
			$fork->kill;
		}
	});
};

$SIG{TERM} = \&safeKill;

$fork->on(read => sub {
	my ($fork, $buffer) = @_;
	warn 'fork pid is '. $fork->pid;

	$on_read_flag = 1;
	my $delay = Mojo::IOLoop->delay(sub {
		warn "on read done";
		$on_read_flag = 0;
	});

	for my $line (split /\n/, $buffer)
	{
		my $end = $delay->begin;
		processBuffer($line, sub {
			$end->();
		});
	}
});

$fork->start(
	program => 'ssh',
	program_args => ['-p', $ssh_port, "$username\@$ssh_host", 'gerrit', 'stream-events'],
	conduit => 'pipe',
);

my $reactor = $fork->ioloop->reactor;
warn 'reactor start...';

$reactor->start unless $reactor->is_running;
